課程:JavaScript 與 React 底層原理 第 4 堂:Closure 收尾與複習
Lesson 4 note
本節課深入探討了 Closure 在 React 中的實務應用(useState 與 useEffect),並針對整個 Topic 2(Closure 與記憶體管理)進行了完整的主動回想與概念統整。
Closure 與 React 的連結
- 渲染本質是全新的函數呼叫
- Function Component 每次渲染都會建立一個全新的執行環境(EC),並產生該次渲染專屬的狀態快照。
- 不同渲染次數間的變數是彼此獨立的常數,而非可變的共享變數。
- useState 的閉包機制
setCount函數本質上是一個閉包,它在定義時就已「捕獲」了對應 Fiber 節點的參考,因此不需要額外參數就能精準更新狀態。
- Capture Value(捕獲值)與 Stale Closure
- 元件內的函數(如事件處理器或 Effect)會捕獲其定義當下的作用域內容。
- 當異步任務(如
setTimeout)或依賴陣列不正確的useEffect引用了舊渲染環境的變數時,就會產生讀取到舊值的「過時閉包」現象。 - React 的設計哲學
- 利用閉包鎖定快照是為了確保「可預測性」。這保證了在單次渲染的邏輯脈絡中,所有的 UI 與副作用所看到的狀態都是一致的。
Review
- 詞法作用域(Lexical Scope)的底層貫穿
- 複習了函數如何記住「定義時」的環境(環境背包),這解釋了為什麼在 React 中依賴陣列缺失會導致讀取到初始值。
- 記憶體管理與可達性原則
- 強調閉包捕獲的是整個「環境紀錄(Environment Record)」。即使閉包沒直接使用某個大型變數,只要該變數存在於同一個環境且閉包被全域引用,該變數就無法被回收。
- 解決閉包陷阱的策略
- 透過「函數式更新(Functional Update)」將逻辑交還給 React 內部狀態,或使用
useRef繞過閉包的常數快照特性,直接讀取物件參考。
- 透過「函數式更新(Functional Update)」將逻辑交還給 React 內部狀態,或使用
Q&A
- Q: 如果在
useEffect中監聽點擊事件但依賴陣列為空,為什麼抓不到更新後的count?- 核心洞察: 這是因為函數的 Scope Chain 是在「定義時」決定的。
- 由於
useEffect只在掛載(mount)時執行一次,內部的事件處理函數是在第一次渲染的 EC 中「出生」的。 - 它所連結的環境背包永遠停留在第一次渲染的那一刻,無論畫面重新渲染幾次,這個舊函數查找到的永遠是定義時的初始值。
- Q: 若閉包函數完全沒用到父環境中的巨大陣列,該陣列還會佔用記憶體嗎?
- 核心洞察: 會。因為閉包捕獲的是「整個環境紀錄」,而非個別變數。
- 只要該閉包函數是「可達的(Reachable)」(例如被存放在全域變數中),它所連結的整個父環境紀錄(包含那個沒被使用的 100MB 陣列)都會被視為在使用中,導致垃圾回收(GC)無法釋放該記憶體。
- Q: 在不修改
useEffect依賴陣列的情況下,如何讓setInterval讀取到最新狀態?- 核心洞察: 使用「函數式更新(Functional Update)」如
setCount(prev => prev + 1)。
- 核心洞察: 使用「函數式更新(Functional Update)」如
- 這種方法不需要依賴閉包捕獲的外部變數,而是直接向 React 內部的「控制室」請求當前的最新狀態值,從而完美繞過過時閉包的限制。